module ActiveMerchant
  module Billing
    module AuthorizeNetCimAPI
      
      def self.included(base)
        Base.generate_all_attr_accessors!
      end
      
      class Base
        include Validateable
        class_inheritable_accessor :scalar_attributes, :composite_attributes
        self.scalar_attributes = [] 
        self.composite_attributes = []

        def intialize(options = {})
          options.each do |attribute, value|
            send(:"#{attribute}=", value) if respond_to? attribute
          end
        end

        def self.generate_all_attr_accessors!
          (self.subclasses || []).each do |sc|
            sc = sc[/^.*::(.*)$/,1]
            klass = ActiveMerchant::Billing::AuthorizeNetCimAPI.const_get(sc)
            klass.generate_all_attr_accessors!
            klass.define_attribute_accessors
          end
        end

        def to_xml(xml = nil)
          xml ||= Builder::XmlMarkup.new :indent => 2

          self.class.scalar_attributes.each do |attr|
            attribute, value = attr[0], send(attr[0]).to_s
            xml.tag! attribute, value unless value.blank?
          end

          self.class.composite_attributes.each do |attr|
            attribute, value = attr[0], send(attr[0])
            unless value.blank?
              if value.is_a? Array
                value.each do |val|
                  xml.tag! attribute do
                    val.to_xml(xml)
                  end
                end
              else
                xml.tag! attribute do
                  value.to_xml(xml)
                end
              end
            end
          end

          xml
        end

        def self.define_attribute_accessors
          attr_symbols = (self.scalar_attributes + self.composite_attributes).map{|attr| ":#{attr.first}"}.join(',')
          self.instance_eval("attr_accessor #{attr_symbols}")

          self.scalar_attributes.each do |attr|
            attribute, klass = attr[0].to_s, attr[2]
            self.class_eval %Q{
              def #{attribute}=(data)
                @#{attribute} = data.to_s
              end
            }
          end

          self.composite_attributes.each do |attr|
            attribute, klass = attr[0].to_s, attr[2]

            self.class_eval %Q{
              def #{attribute}=(data)
                if data.is_a? Array
                  data.each_with_index do |val, i|
                    if val.is_a? Hash
                      data[i] = val = #{klass}.new(val)
                    end
                    raise ArgumentError, "Data must either be a Hash or a #{klass}" unless (val.is_a?(#{klass}) || val.blank?)
                  end
                end
                
                if data.is_a? Hash
                  data = #{klass}.new(data)
                end
                
                raise ArgumentError, "Data must either be a Hash, Array, or #{klass}" unless (data.is_a?(#{klass}) || data.is_a?(Array) || data.blank?)
                @#{attribute} = data
              end
            } if klass
          end
        end

        def scalar_attributes
          self.class.scalar_attributes
        end

        def composite_attributes
          self.class.composite_attributes
        end

        def validate
          validate_scalar_attributes
          validate_composite_attributes
          validate_custom if respond_to? :validate_custom
        end

        def validate_scalar_attributes
          self.class.scalar_attributes.each do |attr|
            attribute, required, condition, message = *attr[0..3]
            value = send(attribute)

            if value.blank?
              next if not required
              errors.add attribute, "is required"
              next
            end

            if condition.is_a?(Integer) && value.length > condition    # maximum number of characters
              errors.add attribute, (message || "can't be greater than #{condition} characters")
            elsif condition.is_a?(Array) && !condition.include?(value.to_sym)    # specific enumeration
              errors.add attribute, (message || "must be one of: #{condition.join(', ')}")
            elsif condition.is_a?(Regexp)
              errors.add attribute, (message || "doesn't match the required format") unless value =~ condition
            end

          end
        end

        def self.format_attr(attr)
          attribute, required, condition, message = *attr[0..3]
          required = required ? 'required' : 'not required'


          message = if condition.is_a?(Integer)
            (message || "can't be greater than #{condition} characters")
          elsif condition.is_a?(Array)
            (message || "must be one of: #{condition.join(', ')}")
          elsif condition.is_a?(Regexp)
            (message || "must be of format #{condtiion}")
          end

          '%s - %s - %s' % [attribute, required, message]
        end

        def validate_composite_attributes
          self.class.composite_attributes.each do |attr|
            attribute, required, value = attr[0], attr[1], send(attr[0])
            if value.blank?
              next if not required
              errors.add attribute, "is required"
              next
            end
                      
            if value.is_a? Array
              value.each do |val|
                errors.add attribute, "is invalid" unless val.valid?
              end
            else
              errors.add attribute, "is invalid" unless value.valid?
            end
          end
        end

        def self.schema(indent = 2)
          str = "#{self}\n"
          scalar_attributes.each do |attr|
            str << "#{' '*indent}#{format_attr(attr)}\n"
          end
          composite_attributes.each do |attr|
            str << "#{' '*indent}#{attr[0]} (#{attr[1]})=> #{attr.last.schema(indent+2)}"
          end
          str
        end
        
        def full_aggregate_errors
          full_errors = errors.dup
          composites = composite_attributes.flatten
          full_errors.keys.each do |attribute|
            attribute = attribute.to_sym
            if composites.include? attribute
              full_errors[attribute] = send(attribute).full_aggregate_errors
            end
          end
          full_errors
        end
        
      end 
    end
  end
end